/**

Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     licenses@blazegraph.com

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
/*
 * Created on Feb 4, 2007
 */

package com.bigdata.journal;

import java.io.File;
import java.nio.ByteBuffer;
import java.util.Properties;
import java.util.Random;

import com.bigdata.rawstore.IRawStore;
import com.bigdata.rwstore.IRWStrategy;
import com.bigdata.util.InnerCause;

/**
 * Test suite for restart-safe (data survives commit and reopen of the store).
 * 
 * @todo verify {@link ICommitter} protocol.
 * 
 * @todo verify nextOffset after restart and other metadata preserved by the
 * root blocks.
 * 
 * @todo verify {@link IBufferStrategy#truncate(long)}.  note that you can not
 * extend a mapped file.  note that the Direct mode must extend both the file
 * and the buffer while the Disk mode only extends the file.
 * 
 * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
 * @version $Id$
 */
abstract public class AbstractRestartSafeTestCase extends AbstractBufferStrategyTestCase {

    public AbstractRestartSafeTestCase() {
    }

    public AbstractRestartSafeTestCase(String name) {
        super(name);
    }

//    /**
//     * override to disable deletion of the store on close.
//     */
//    public Properties getProperties() {
//        
//        Properties properties = super.getProperties();
//        
//        properties.setProperty(Options.DELETE_ON_CLOSE,"false");
//
//        return properties;
//        
//    }
    
    /**
     * Re-open the same backing store.
     * 
     * @param store
     *            the existing store.
     * 
     * @return A new store.
     * 
     * @exception Throwable
     *                if the existing store is not closed, e.g., from failure to
     *                obtain a file lock, etc.
     */
    protected IRawStore reopenStore(final IRawStore store) {

        boolean closedForWrites = false;
        
        if (store.isReadOnly() && store instanceof Journal
                && ((Journal) store).getRootBlockView().getCloseTime() != 0L) {

            closedForWrites = true;
            
        }

        // close the store.
        store.close();
        
        // Note: Clone to avoid modifying!!!
        final Properties properties = (Properties)getProperties().clone();
        
        // Turn this off now since we want to re-open the same store.
        properties.setProperty(Options.CREATE_TEMP_FILE,"false");
        
        // The backing file that we need to re-open.
        final File file = store.getFile();
        
        assertNotNull(file);
        
        // Set the file property explicitly.
        properties.setProperty(Options.FILE,file.toString());
        
        if(closedForWrites) {

            /*
             * This supports unit tests which use closeForWrites() and then
             * reopen the journal.
             */
            properties.setProperty(Options.READ_ONLY,"true");
            
        }
        
        return new Journal( properties );
        
    }

    /**
     * Writes a record, verifies the write but does NOT commit the store. Closes
     * and reopens the store and finally verifies the write was lost.
     */
    public void test_restartSafe_oneWriteNoCommit() {

        IAtomicStore store = (IAtomicStore) getStore();

        try {
            
            assertTrue(store.isStable());

            final Random r = new Random();

            final int len = 100;

            final byte[] expected = new byte[len];

            r.nextBytes(expected);

            final ByteBuffer tmp = ByteBuffer.wrap(expected);

            final long addr1 = store.write(tmp);

            // verify that the position is advanced to the limit.
            assertEquals(len, tmp.position());
            assertEquals(tmp.position(), tmp.limit());

            // read the data back.
            final ByteBuffer actual = store.read(addr1);

            assertEquals(expected, actual);

            /*
             * verify the position and limit after the read.
             */
            assertEquals(0, actual.position());
            assertEquals(expected.length, actual.limit());

            /*
             * DO NOT COMMIT.
             */

            // re-open the store.
            store = (IAtomicStore) reopenStore(store);

            assertTrue(store.isStable());

            /*
             * attempt read the data back. this should throw an exception since
             * the nextOffset in the root block will still be zero and the store
             * can therefore correctly reject this address as never written.
             */
            try {
                store.read(addr1);
                fail("Expecting: " + IllegalArgumentException.class);
            } catch (RuntimeException ex) {
                if (InnerCause.isInnerCause(ex,
                        IllegalArgumentException.class)) {
                    if (log.isInfoEnabled())
                        log.info("Ignoring expected exception: " + ex);
                } else {
                    fail("Expecting inner cause: "
                            + IllegalArgumentException.class
                            + ", not: " + ex, ex);
                }
            }

        } finally {

            store.destroy();

        }

    }

    /**
     * Writes a record, verifies the write then commits the store. Closes and
     * reopens the store and finally verifies the write on the reopened store.
     */
    public void test_restartSafe_oneWrite() {
        
        IAtomicStore store = (IAtomicStore)getStore();
        
        try {

            assertTrue(store.isStable());

            final Random r = new Random();

            final int len = 100;

            final byte[] expected = new byte[len];

            r.nextBytes(expected);

            final ByteBuffer tmp = ByteBuffer.wrap(expected);

            final long addr1 = store.write(tmp);

            // verify that the position is advanced to the limit.
            assertEquals(len, tmp.position());
            assertEquals(tmp.position(), tmp.limit());

            // read the data back.
            ByteBuffer actual = store.read(addr1);

            assertEquals(expected, actual);

            /*
             * verify the position and limit after the read.
             */
            assertEquals(0, actual.position());
            assertEquals(expected.length, actual.limit());

            /*
             * Commit the changes - if you do not commit the changes then the
             * root blocks are not updated and your data is lost on restart.
             */
            store.commit();

            // re-open the store.
            store = (IAtomicStore) reopenStore(store);

            assertTrue(store.isStable());

            // read the data back.
            actual = store.read(addr1);

            assertEquals(expected, actual);

        } finally {

            store.destroy();

        }

    }
    
    /**
     * Test writes a bunch of records and verifies that each can be read after
     * it is written.  The test then performs a random order read and verifies
     * that each of the records can be read correctly.
     */
    public void test_restartSafe_multipleWrites() {

        IAtomicStore store = (IAtomicStore)getStore();
        
        try {
        
        assertTrue(store.isStable());

        Random r = new Random();

        /*
         * write a bunch of random records.
         */
        final int limit = 100;
        
        final long[] addrs = new long[limit];
        
        final byte[][] records = new byte[limit][];
        
        for(int i=0; i<limit; i++) {

            byte[] expected = new byte[r.nextInt(100) + 1];
        
            r.nextBytes(expected);
        
            ByteBuffer tmp = ByteBuffer.wrap(expected);
            
            long addr = store.write(tmp);

            // verify that the position is advanced to the limit.
            assertEquals(expected.length,tmp.position());
            assertEquals(tmp.position(),tmp.limit());

            assertEquals(expected,store.read(addr));
        
            addrs[i] = addr;
            
            records[i] = expected;
            
        }

        /*
         * now verify data with random reads.
         */

        int[] order = getRandomOrder(limit);
        
        for(int i=0; i<limit; i++) {
            
            long addr = addrs[order[i]];
            
            byte[] expected = records[order[i]];

            assertEquals(expected,store.read(addr));
            
        }
        
        /*
         * Commit the changes - if you do not commit the changes then the root
         * blocks are not updated and your data is lost on restart.
         */
        store.commit();
        
        // re-open the store.
        store = (IAtomicStore)reopenStore(store);
        
        assertTrue( store.isStable() );

        /*
         * now verify data with random reads.
         */

        order = getRandomOrder(limit);
        
        for(int i=0; i<limit; i++) {
            
            long addr = addrs[order[i]];
            
            byte[] expected = records[order[i]];

            assertEquals(expected,store.read(addr));
            
        }

        } finally {

            store.destroy();
            
        }
        
    }

    /**
     * Test of abort semantics.
     */
    public void test_abort() {

        class AbortException extends RuntimeException {
            private static final long serialVersionUID = 1L;
        }

        final IAtomicStore store = (IAtomicStore) getStore();

        try {

            // write some data onto the store.
            for (int i = 0; i < 100; i++) {
                
                store.write(getRandomData());
                
            }

            // trigger an abort.
            throw new AbortException();

        } catch (AbortException ex) {

            // discard the write set.
            store.abort();

            /*
             * write different data onto the store (just to verify that it is
             * still functional).
             */
            for (int i = 0; i < 100; i++) {
                store.write(getRandomData());
            }
            
        } catch (Throwable t) {

            // discard the write set.
            store.abort();

            fail("Unexpected exception: " + t, t);

        } finally {

            store.destroy();

        }

    }

    /**
     * Unit tests writes some data, commits, and closes the journal against
     * future writes. The {@link LRUNexus} is then cleared and we verify that we
     * can still read data back from the store. Finally, we close and then
     * reopen the store in a read-only mode and verify that we can still read on
     * the store.
     * <p>
     * This test was written to verify that closing the journal against future
     * writes does not leave the write cache in an unusable state (e.g., if it
     * is discarded, then we do not attempt to read against the write cache).
     * Since {@link AbstractJournal#closeForWrites(long)} does not interfere
     * with existing readers, care must be exercised if we are to release the
     * write cache atomically.
     * 
     * @todo test also with a concurrent reader since concurrent close of the
     *       write cache could be a problem.
     */
    public void test_closeForWrites() {
        
        Journal store = (Journal) getStore();
        
        try {

            if (store.getBufferStrategy() instanceof IRWStrategy)
                return; // void test

            final int nrecs = 1000;
            final ByteBuffer[] recs = new ByteBuffer[nrecs];
            final long addrs[] = new long[nrecs];

            // Write a bunch of data onto the store.
            for (int i = 0; i < nrecs; i++) {

                recs[i] = getRandomData();

                addrs[i] = store.write(recs[i]);

            }

            // commit
            final long lastCommitTime = store.commit();

            // close against further writes.
            store.closeForWrites(lastCommitTime/* closeTime */);

//            if (LRUNexus.INSTANCE != null) {
//
//                // discard the record level cache so we will read through.
//                LRUNexus.INSTANCE.deleteCache(store.getUUID());
//
//            }

            // Verify read back.
            for (int i = 0; i < nrecs; i++) {

                final long addr = addrs[i];

                final ByteBuffer expected = recs[i];

                // position := 0, limit := capacity;
                expected.clear();

                final ByteBuffer actual = store.read(addr);

                assertEquals(expected, actual);

            }
            
            if(store.isStable()) {
                
                store = (Journal) reopenStore(store);

                // Verify read back after re-open.
                for (int i = 0; i < nrecs; i++) {

                    final long addr = addrs[i];

                    final ByteBuffer expected = recs[i];

                    // position := 0, limit := capacity;
                    expected.clear();

                    final ByteBuffer actual = store.read(addr);

                    assertEquals(expected, actual);

                }
                
            }
            
        } finally {
            
            store.destroy();
            
        }
        
    }
    
}
